Skip to content

The sidebar stops spending columns on nothing, and a background tab says whose it is - #33

Merged
HarryCordewener merged 3 commits into
mainfrom
feat/rail-width-and-tab-chips
Aug 12, 2026
Merged

The sidebar stops spending columns on nothing, and a background tab says whose it is#33
HarryCordewener merged 3 commits into
mainfrom
feat/rail-width-and-tab-chips

Conversation

@HarryCordewener

@HarryCordewener HarryCordewener commented Aug 12, 2026

Copy link
Copy Markdown
Member

Two things reported while reading a frame, fixed separately and reviewable apart (one commit each).

1. The connection rail was too wide for what it holds

c9aa311. The demo's connections frame drew a 22-column sidebar whose widest visible row ended at column 20 and whose typical row ended at 15. Two of those columns named nothing:

  • The indent ladder skipped a level. Worlds sat at indent 0, characters jumped to 2, windows to 3 — so every window row carried two cells of indent for a depth no row is ever drawn at. It is one level per depth now: world 0, character 1, window 2.
  • RailMargin was 2. A divider column and a one-cell spacer already stand between the rail's last cell and the first pane, so the second margin cell separated nothing from nothing. It is 1.

The rail's width comes out of the pane area and every connected session is told its pane's size over NAWS, so both are columns handed back to the game. Demo connections: 22 → 19.

The reserved fields are untouched — the unsent pen, the unread badge and the chord column still cost their cells when empty, because a field that appears out of nothing resizes the sidebar from the wire.

Pins: RailModelTests.TheIndentLadderSkipsNoLevel (no row is more than one level deeper than the row before it) and RailWindowRowTests.TheRailSpendsAtMostOneBlankColumnPastItsWidestRow. One existing assertion had the old 4-cell indent baked into a string literal.

2. A background tab wore the wrong character's colour

6a32756. A pane can host several characters' windows as tabs and paints one rectangle, so the plane behind a strip can only ever answer for the window in front. Every other tab in that strip was drawn in that same hue — naming the wrong character, which is the one thing the pane tint exists to prevent.

TabControl's four chip colours belong to the control, so this cannot come from PaintTabChips. It goes where the per-tab channel is: the title, which is markup, where a tag costs no cells and moves no hit test. TabChip carries the plane and its ink; TabTitles.For emits [{ink} on {plane}] around the same span the bold and the activity tint already cover.

Three things hold it together:

  • ChipFor runs the same pipeline as the plane behind itSurfaceToneIn (the character's tint, plus the focus lift when the pane holds focus) then Recessed — so a strip whose tabs share an owner comes out byte for byte what it was, and only a mixed pane changes. PaneSurfaceTone is now one line of that same function rather than a second arithmetic beside it.
  • The selected tab is never chipped: the strip already paints it in its page's plane.
  • Unread stays a foreground over the chip, so "whose" and "something new" remain the two channels they are everywhere else.

A window nobody owns (the web view) and a character who has chosen no colour both come out on the plain surface rather than borrowing a neighbour's hue.

New tint-tabs snapshot view: the two tinted characters of tint with no split, so one pane holds both their windows. It is the only geometry where an idle chip can be seen wearing a colour the pane behind it is not. The frame reads Thistle ⌁ │ Corvid - Chat (2) │ ▌ Corvid with Thistle on Moss, Chat on recessed Slate and the selected Corvid on lit Slate.

PaneTintTests.ABackgroundTabWearsItsOwnCharactersColour reads the painted cells: the old code was internally consistent while the screen was wrong, so anything built on the expression would have passed. Confirmed it discriminates by temporarily resolving the pane's front window instead — it failed with expected Rgb(20,42,14), found Rgb(17,34,71). TabTitlesTests pins the markup's shape and that a chip costs no cells.

Verification

dotnet build SharpMUTerm.slnx warning-free, and all five suites green: Core 937, Graphics 83, Scripting 42, Web 37, Tui 1777.

🤖 Generated with Claude Code

https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN

Summary by CodeRabbit

  • New Features

    • Tab chips now use each character’s tint, including distinct colors for idle tabs.
    • Unowned tabs use the neutral recessed surface.
    • Mixed-character tab layouts are supported in tint snapshot views.
    • Workspace rail indentation and width are more compact and consistent.
  • Bug Fixes

    • Preserved selected-tab styling and unread indicators.
    • Corrected rail spacing and indentation behavior.
    • Improved tab title styling without affecting title width.

HarryCordewener and others added 2 commits August 12, 2026 15:36
The connection rail was 22 columns wide in the demo's `connections` frame
while its widest visible row ended at 20 and most ended at 15. Two of those
columns named nothing.

The indent ladder skipped a level: worlds at 0, characters straight to 2,
windows at 3, so every window row carried two cells of indent for a depth
no row is ever drawn at. It is one level per depth now — world 0,
character 1, window 2.

`RailMargin` goes 2 → 1. A divider column and a one-cell spacer already
stand between the rail's last cell and the first pane, so the second margin
cell separated nothing from nothing.

The rail's width comes out of the pane area and every connected session is
told its pane's size over NAWS, so both are columns handed back to the
game. Demo `connections` rail: 22 → 19.

The reserved fields are untouched — the unsent pen, the unread badge and
the chord column still cost their cells whether or not they have anything
to say, because a field that appears out of nothing resizes the sidebar
from the wire.

Two pins: `TheIndentLadderSkipsNoLevel` (no row is more than one level
deeper than the row before it) and `TheRailSpendsAtMostOneBlankColumnPast
ItsWidestRow`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
A pane can host several characters' windows as tabs and paints one
rectangle, so the plane behind a strip can only ever answer for the window
in front. Every other tab in that strip was drawn in that same hue —
naming the wrong character, which is the one thing the pane tint exists to
prevent.

`TabControl`'s four chip colours belong to the control, so this cannot come
from `PaintTabChips`. It goes where the per-tab channel is: the title, which
is markup, where a tag costs no cells and moves no hit test. `TabChip`
carries the plane and its ink, and `TabTitles.For` emits `[{ink} on {plane}]`
around the same span the bold and the activity tint already cover.

`ChipFor` runs the same pipeline as the plane behind it — `SurfaceToneIn`
(the character's tint, plus the focus lift when the pane holds focus) then
`Recessed` — so a strip whose tabs share an owner comes out byte for byte
what it was, and only a mixed pane changes. `PaneSurfaceTone` is now one
line of that function rather than a second arithmetic beside it. The
selected tab is never chipped: the strip already paints it in its page's
plane. Unread stays a foreground over the chip, so "whose" and "something
new" remain two channels. A window nobody owns and a character who has
chosen no colour come out on the plain surface.

New `tint-tabs` view: the two tinted characters of `tint` with no split, so
one pane holds both their windows. It is the only geometry where an idle
chip can be seen wearing a colour the pane behind it is not.

`PaneTintTests.ABackgroundTabWearsItsOwnCharactersColour` reads the painted
cells — the old code was internally consistent while the screen was wrong,
so nothing built on the expression would have caught it. `TabTitlesTests`
pins the markup's shape and that a chip costs no cells.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e5068b49-4e89-49d9-9598-e165478b02a5

📥 Commits

Reviewing files that changed from the base of the PR and between 6a32756 and 8a62a06.

📒 Files selected for processing (3)
  • tests/SharpMUTerm.Core.Tests/Workspace/RailModelTests.cs
  • tests/SharpMUTerm.Tui.Tests/PaneTintTests.cs
  • tests/SharpMUTerm.Tui.Tests/RailWindowRowTests.cs

Walkthrough

The change adds per-character tinting for idle tab chips, preserves selected and unread tab behavior, and adds tint-tabs snapshot coverage. It also updates rail indentation and width calculations to match the world, character, and window depth model.

Changes

Per-character tab chip tinting

Layer / File(s) Summary
Tab chip contract and rendering
src/SharpMUTerm.Tui/TabTitles.cs, tests/SharpMUTerm.Tui.Tests/TabTitlesTests.cs
TabTitles.For accepts optional TabChip styling for idle tabs. Selected tabs retain page styling, unread tabs retain activity foreground tint, and chip markup does not affect title width.
Tint pipeline integration and verification
src/SharpMUTerm.Tui/SharpMUTermApp.cs, tests/SharpMUTerm.Tui.Tests/PaneTintTests.cs, CLAUDE.md
Tab chips use each window’s character tint and pane focus state during initial creation and refresh. The tint-tabs snapshot and painted-frame tests cover the behavior.

Rail indentation and spacing

Layer / File(s) Summary
Rail depth and width alignment
src/SharpMUTerm.Core/Workspaces/RailModel.cs, src/SharpMUTerm.Tui/SharpMUTermApp.cs, tests/SharpMUTerm.Core.Tests/Workspace/RailModelTests.cs, tests/SharpMUTerm.Tui.Tests/RailWindowRowTests.cs, tests/SharpMUTerm.Tui.Tests/RailRendererTests.cs
World, character, and window rows use indentation levels 0, 1, and 2. The expanded rail leaves one trailing blank column, and related width assertions are updated.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SharpMUTermApp
  participant TabTitles
  participant TerminalFrame
  SharpMUTermApp->>SharpMUTermApp: derive TabChip from window tint and pane focus
  SharpMUTermApp->>TabTitles: build or refresh tab title with TabChip
  TabTitles->>TerminalFrame: render per-character idle tab styling
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes both primary changes: reduced sidebar width and character-specific styling for background tabs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/SharpMUTerm.Core.Tests/Workspace/RailModelTests.cs`:
- Around line 212-233: Strengthen TheIndentLadderSkipsNoLevel by asserting each
row’s exact indent based on its Kind: worlds at 0, character and empty rows at
1, and windows at 2. Keep the existing ordering and maximum-indent checks, and
apply the assertions while iterating over rows so incorrect empty-row
indentation is detected.

In `@tests/SharpMUTerm.Tui.Tests/PaneTintTests.cs`:
- Around line 135-175: Add a companion frame test near
ABackgroundTabWearsItsOwnCharactersColour that renders a single pane containing
a tinted window and an unowned window such as the web view, with the unowned
window backgrounded. Locate both tab labels via FrameGrid.Decode and assert the
unowned chip’s background is
WorkspacePalette.Recessed(WorkspacePalette.Focus(WorkspacePalette.Tint(config.Theme,
PaneTint.None))) while the tinted chip uses its own tint, confirming the colours
differ.

In `@tests/SharpMUTerm.Tui.Tests/RailWindowRowTests.cs`:
- Around line 244-262: Update TheRailSpendsAtMostOneBlankColumnPastItsWidestRow
to assert rendered geometry rather than only comparing RailColumnWidth with the
widest logical rail row. After RenderSnapshot, inspect the arranged rail bounds
and the adjacent pane rectangle (or NAWS-derived width), and verify the
one-column margin relationship using those actual layout values.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6957108e-8949-4423-bf61-a463866db137

📥 Commits

Reviewing files that changed from the base of the PR and between 49d4fed and 6a32756.

📒 Files selected for processing (9)
  • CLAUDE.md
  • src/SharpMUTerm.Core/Workspaces/RailModel.cs
  • src/SharpMUTerm.Tui/SharpMUTermApp.cs
  • src/SharpMUTerm.Tui/TabTitles.cs
  • tests/SharpMUTerm.Core.Tests/Workspace/RailModelTests.cs
  • tests/SharpMUTerm.Tui.Tests/PaneTintTests.cs
  • tests/SharpMUTerm.Tui.Tests/RailRendererTests.cs
  • tests/SharpMUTerm.Tui.Tests/RailWindowRowTests.cs
  • tests/SharpMUTerm.Tui.Tests/TabTitlesTests.cs

Comment thread tests/SharpMUTerm.Core.Tests/Workspace/RailModelTests.cs
Comment thread tests/SharpMUTerm.Tui.Tests/PaneTintTests.cs
Comment thread tests/SharpMUTerm.Tui.Tests/RailWindowRowTests.cs
All three were the same shape — a test that agreed with the code it was
watching rather than with the screen or the model.

`TheIndentLadderSkipsNoLevel` proved the ladder was monotonic and capped at
2, which an Empty row at indent 0 satisfies. Each kind is now held to its
own depth.

`TheRailSpendsAtMostOneBlankColumnPastItsWidestRow` was arithmetic over the
rail's own rows and would have passed had nothing applied the answer. It
closes against the arranged pane rectangle now: a pane starts past the
rail, its divider and the spacer — the same geometry per-pane NAWS is
derived from.

`AnUnownedBackgroundTabBorrowsNobodysColour` is new: the other half of the
chip rule, that a tab nobody owns stays on the plain surface rather than
borrowing the hue of whatever is in front of it. The web view is the
reachable case and the one that would make the cue name a character whose
window it is not. It asserts the pane really is tinted too, or the frame
asks nothing; confirmed it fails against the old per-pane chip with
`expected Rgb(41,41,46), found Rgb(17,34,71)`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
@HarryCordewener
HarryCordewener merged commit 8a16d5c into main Aug 12, 2026
3 checks passed
@HarryCordewener
HarryCordewener deleted the feat/rail-width-and-tab-chips branch August 12, 2026 21:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant